﻿using System;
using System.Collections;
using System.Collections.Generic;

using Yawf.Core.Exceptions;
using Yawf.Core.Reflection;
using Yawf.Core.Utils;

namespace Yawf.Web.Utils {
	public class RequestDataMaterializer {
		// Private fields
		private RequestWrapper requestWrapper;
		private IObjectFinder objectFinder;
		private IList<String> classPaths;

		// Properties
		public RequestWrapper RequestWrapper { get { return requestWrapper; } set { requestWrapper = value; } }
		public IObjectFinder ObjectFinder { get { return objectFinder; } set { objectFinder = value; } }
		public IList<String> ClassPaths { get { return classPaths; } set { classPaths = value; } }

		/// <summary>
		/// Creates a new RequestDataMaterializer with given RequestWrapper,
		/// IObjectFinder and ClassPaths
		/// </summary>
		/// <param name="wrapper">RequestWrapper to pull Request objects out from</param>
		/// <param name="finder">Object finder to lookup objects by values(ids)</param>
		/// <param name="paths">Classpath to look in when creating instances of object</param>
		public RequestDataMaterializer(RequestWrapper wrapper,IObjectFinder finder,IList<String> paths) {
			RequestWrapper = wrapper;
			ObjectFinder = finder;
			ClassPaths = paths;
		}
		
		/// <summary>
		/// Convienient method to use. This method will call the getObject with
		/// a class parameter after checking that the type is not a simple type.
		/// If type is a simple type it will return the value directly from 
		/// RequestDataFetcher, no need to materialize cause its a simple value
		/// </summary>
		/// <param name="name">Name of object to materialize</param>
		/// <param name="type">Type of object to materialize</param>
		/// <param name="defaultValue">defaultValue for simple type</param>
		/// <returns>a materialized object, taking data from internal 
		/// RequestDataFetcher object</returns>
		public Object GetObject(String name,String type,String defaultValue) {
			try {
				// Check first if we really want a simple type from request first
				if (ReflectionUtils.IsSimpleType(type)) {
					if (defaultValue == null) {
						defaultValue = "";
					}
					return ReflectionUtils.GetSimpleType(type,RequestWrapper.GetString(name,defaultValue));
				}

				// Another type of object are needed, do real materialiing here
				return GetObject(name,Type.GetType(type));
			} catch (Exception e) {
				// Just throw a PubliguiasException. let other take care of this error
				throw new YawfException("Error loading class [" + type + "]", e);
			}
		}

		/**
		* Materializes an object taking data to fill object properties from the internal 
		* RequestDataFetcher object. This method will try to "find" out by name convention
		* the fields name from the RequestDataFetcher object. The look up of fields by name
		* use the following algorithm. 
		* 
		* In the 	RequestDataFetcher object there must exist values with name according to the 
		* following expression
		* 	<name>.<property1>[.<property2>[.<property3>]]
		* 
		* Where <name> is the principal name given and <propertyN> are properties 
		* of the class of given type
		* The <property1> is set directly if the property is of simple type
		* If the <property1> is a complex type and <property2> is not given this method
		* will try to look up the object by the "id" field, supposing that the the resulting 
		* object has an id field. 
		* If <property2> is given this property will be used to do the look up in the database
		* If <property1> is of Collection type then there are 2 possibilities depending if
		* the type uses generics or not. If the property uses a generic then the type of object
		* that should be looked for in database are using introspection and <property2> is used
		* only if the field to do look up with is diffent than "id".
		* If <property1> is of Collection type and generics is not used then <property2> is
		* used to indicate Class Name(only class name, the package that will be appended) 
		* must have been defined in internal list of class pathes) of objects in Collection. The <property3> will in this case 
		* be used to indicate look up field for given class if the field is diferent than "id"
		* 
		* @param name - principal name of object to materialize
		* @param type - class of object to materialize
		* @return a materialized object, taking data from internal RequestDataFetcher object
		* @throws PubliguiasException - If any descending class cannot be found
		**/
		private object GetObject(String name,Type type) {
			IList<String> paramNames;
			Object theObject = null;
			String objectId;
			
			// Create instance of object && and get all parameters that starts with name
			theObject = ReflectionUtils.CreateInstance(type);
			paramNames = RequestWrapper.GetParameterNames(name + ".");
			
			if (theObject is IList){
				// Look up list of objects 
				theObject = LookupList(paramNames,name,true);
			}else if (theObject is DateTime){
				// Its a date 
				theObject = RequestWrapper.GetDateTime(name,"dd/MM/yyyy",null);
			}else{
				// Check weather there exists a value in fetcher with just name,
				// if so obtain object from database
				objectId = RequestWrapper.GetString(name,"");
				if (objectId != null && objectId != "" && ObjectFinder != null){
					theObject = ObjectFinder.LookupInstance(type,"id",objectId);
				}
				// Set properties of object
				SetObjectProperties(theObject,paramNames,name);
			}
			
			return theObject;
		}												// GetObject

		/// <summary>
		/// Sets properties of given object, taking values from request according to
		/// algorithm of name convention.
		/// </summary>
		/// <param name="theObject">object to set properties on</param>
		/// <param name="paramNames">parameter names existing in request</param>
		/// <param name="name">Root name of all parameters of object</param>
		private void SetObjectProperties(Object theObject,IList<String> paramNames,String name){
			Type propertyType;
			Object tmpParam;
			String[] paramParts;
			String paramName;
			String lookupProperty;
			int i;
			
			// Loop through all parameter names and set corresponding value in object
			for(i = 0; paramNames != null && i < paramNames.Count;i++){
				// Get parametr name and a split version
				paramName = paramNames[i];
				paramParts = StringUtils.SplitString(paramName,".");
				
				// Only if we have more than 1 element there is something to do
				tmpParam = null;
				if (paramParts.Length > 1) {
					propertyType = ReflectionUtils.GetPropertyType(theObject,paramParts[1]);
					
					if (typeof(IList).IsAssignableFrom(propertyType)){
						// Its a collection, lookup objects and
						tmpParam = LookupList(paramNames,paramName,false);
					}else if (typeof(DateTime).IsAssignableFrom(propertyType)){
						// Its a date 
						tmpParam = RequestWrapper.GetDateTime("dd/MM/yyyy",paramName,null);
					} else if (ReflectionUtils.IsSimpleType(propertyType.FullName)) {
						// Its a siple type, just get the value
						tmpParam = ReflectionUtils.GetSimpleType(propertyType.FullName,RequestWrapper.GetString(paramName,""));
					}else{
						// An domain object, try to look it up in database
						lookupProperty  = "id";
						if (paramParts.Length > 2){
							lookupProperty = paramParts[2];
						}
						if (ObjectFinder != null) {
							tmpParam = ObjectFinder.LookupInstance(propertyType,lookupProperty,RequestWrapper.GetString(paramName,"-1"));
						}
					}
					
					// Set Property on object instance with created object
					ReflectionUtils.SetProperty(theObject,paramParts[1],tmpParam);
				}
			}
		}									// SetObjectProperties

		/// <summary>
		/// Looks up a collection with elements taking data to look up objects
		/// in colleection from the RequestDataFetcher object, the values 
		/// starts with name
		/// </summary>
		/// <param name="paramNames">list of all parameter names</param>
		/// <param name="name">name of start of parameters</param>
		/// <param name="toplevel">true if we are getting a top level collecction or false
		/// if is a collection to set on a field</param>
		/// <returns>a List with elements, null if no data can be found from 
		/// RequestWrapper</returns>
		private IList LookupList(IList<String> paramNames,String name,bool toplevel) {
			Type type;
			String[] paramParts,values;
			String lookupProperty,paranName;
			int index,least;

			// Get right least variable
			least = 4;
			if (toplevel) {
				least = 3;
			}

			// Find index of a parameter that starts with name.prop1 or name.prop1.prop2
			index = StringUtils.FindStringIndex(paramNames,name,least,"\\.");
			if (index <= -1) {
				index = StringUtils.FindStringIndex(paramNames,name,least - 1,"\\.");
				if (index <= -1) {
					// return, no parameter data can be found
					return null;
				}
			}
			paranName = paramNames[index];
			paramParts = StringUtils.SplitString(paranName,".");

			// Find out look up property
			lookupProperty = "id";
			if (paramParts.Length >= least) {
				lookupProperty = paramParts[least - 1];
			}

			// Find values
			values = RequestWrapper.GetStrings(paranName);
			if (values == null) {
				return null;
			}

			// Find class str
			type = FindType(paramParts[least - 2]);
			if (type == null) {
				return null;
			}

			if (ObjectFinder == null) {
				return null;
			}

			return ObjectFinder.LookupList(type,lookupProperty,values);
		}								// LookupList

		/// <summary>
		/// This method will try to find a full class name of simpleClassName. It will
		/// try to a package name from internal list of posible class path till it will 
		/// find a class that really exists(trying to instanciate with full class path)
		/// </summary>
		/// <param name="simpleClassName">simple class name</param>
		/// <returns>a full class path to an existing class with this name, null if no class exists</returns>
		private Type FindType(String simpleClassName) {
			Object o;
			String[] pathParts;
			int i;

			for (i = 0; ClassPaths != null && i < ClassPaths.Count; i++) {
				try {
					pathParts = StringUtils.SplitString(ClassPaths[i],",");
					o = null;
					if (pathParts != null && pathParts.Length > 1) {
						o = ReflectionUtils.CreateInstance(pathParts[0] + "." + simpleClassName + "," + pathParts[1]);
					} else if (pathParts != null && pathParts.Length > 0) {
						o = ReflectionUtils.CreateInstance(pathParts[0] + "." + simpleClassName);
					}
					if (o != null) {
						return o.GetType();
					}
				} catch (YawfException e) {
					// Do nothing continue with next class path
					e.ToString();
				}
			}

			return null;
		}									// FindType

		/// <summary>
		/// Retrieves a simple type array from request, calls GetDoubles, GetBools etc
		/// </summary>
		/// <param name="name">Name of parameter to retrieve</param>
		/// <param name="paramType">Type of parameter to get</param>
		/// <returns>An array of parameter type values from request</returns>
		public Object GetSimpleTypeArray(String name,String paramType) {
			Object arrayObj;

			if (paramType == "System.Boolean") {
				arrayObj = RequestWrapper.GetBooleans(name);
			} else if (paramType == "System.Byte") {
				arrayObj = RequestWrapper.GetBytes(name);
			} else if (paramType == "System.Char") {
				arrayObj = RequestWrapper.GetChars(name);
			} else if (paramType == "System.Double") {
				arrayObj = RequestWrapper.GetDoubles(name);
			} else if (paramType == "System.Single") {
				arrayObj = RequestWrapper.GetFloats(name);
			} else if (paramType == "System.Int32") {
				arrayObj = RequestWrapper.GetInts(name);
			} else if (paramType == "System.Int64") {
				arrayObj = RequestWrapper.GetLongs(name);
			} else if (paramType == "System.Int16") {
				arrayObj = RequestWrapper.GetShorts(name);
			} else{
				arrayObj = requestWrapper.GetStrings(name);
			}

			return arrayObj;
		}							// GetSimpleTypeArray
	}									// Class
}
